package com.google.code.xconf;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;

public class ConfigurationParser {

    public Configuration parse(InputStream input) throws ParseException {
        PushbackInputStream in = new PushbackInputStream(input);
        Configuration result = new Configuration(null);
        try {
            result.addNodes(parseProperties(null, in));
        } catch (IOException e) {

        }
        return result;
    }

    private Collection<Property> parseProperties(PropertyGroup context, PushbackInputStream in) throws IOException, ParseException {
        boolean propertyGroupClosed = false;
        Collection<Property> result = new ArrayList<>();
        int c = nextNonWhitespaceCharacter(in);
        do {
            if (c == '<') {
                int n = in.read(); // peek next char
                if (n == '/') {
                    String closingName = parseToken(in);
                    if (context == null) {
                        throw new ParseException("Property group closure without a group context, found '" + closingName + "'", in.getPosition());
                    } else if (closingName == null) {
                        throw new ParseException("Missing property group closure, expected '"+ context.getName() + "'", in.getPosition());
                    } else if (!context.getName().equals(closingName)) {
                        throw new ParseException("Property group closure mismatch, found '" + closingName + "' expected '"+ context.getName() + "'", in.getPosition());
                    } else {
                        propertyGroupClosed = true;
                        break;
                    }
                } else {
                    in.unread(n);
                    PropertyGroup group = new PropertyGroup(parseToken(in));
                    group.addValues(parsePropertyValues(in));
                    group.addNodes(parseProperties(group, in));
                    result.add(group);
                }
            } else if (c == '#') {
                skipToNextLine(in);
            } else {
                if (isWhitespace(c) || c == '>') {
                    continue;
                }
                if (c == -1) {
                    break;
                }
                in.unread(c);
                Property p = parseProperty(in);
                if (p != null) {
                    result.add(p);
                }
            }
        } while ((c = nextNonWhitespaceCharacter(in)) != -1);
        if (context != null && !propertyGroupClosed) {
            throw new ParseException("Missing property group closure for '"+ context.getName() + "'", in.getPosition());
        }
        return result;
    }

    private Property parseProperty(PushbackInputStream in) throws IOException, ParseException {
        Property result = null;
        String identifier = parseToken(in);
        if (identifier != null && !identifier.isEmpty()) {
            result = new Property(identifier);
            Collection<String> values = parsePropertyValues(in);
            if (!values.isEmpty()) {
                result.addValues(values);
            }
        }
        return result;
    }

    private Collection<String> parsePropertyValues(PushbackInputStream in) throws IOException, ParseException {
        Collection<String> result = new ArrayList<>();
        int c = in.read();
        do {
            if (c == '>' || c == '\n') {
                break;
            } else if (c == ' ' || c == '\t') {
                String token = parseToken(in);
                if (token != null && !token.isEmpty()) {
                    result.add(token);
                }
            }
        } while ((c = in.read()) != -1);
        return result;
    }

    private String parseToken(PushbackInputStream in) throws IOException, ParseException {
        boolean stringLiteral = false;
        boolean first = true;
        StringBuilder buffer = new StringBuilder();
        int c = nextNonWhitespaceCharacter(in);
        do {
            if (((c == '>' || c == ' ' || c == '\t' || c == '#') && !stringLiteral) || c == '\n') {
                if (c == '#') {
                    skipToNextLine(in);
                } else {
                    in.unread(c);
                }
                break;
            } else if (c == '"') {
                if (stringLiteral) {
                    int n = in.read();
                    in.unread(n);
                    // "hello worl"d
                    if (!isWhitespace(n) && n != '>') {
                        throw new ParseException("Invalid or missing quote in string literal '" + buffer.toString() + "'", in.getPosition());
                    }
                    stringLiteral = false;
                } else {
                    // h"ello world"
                    if (!first) {
                        throw new ParseException("Invalid or missing quote in string literal expression '" + buffer.toString() + "'", in.getPosition());
                    }
                    stringLiteral = true;
                }
            } else {
                buffer.append((char) c);
                if (first) {
                    first = false;
                }
            }
        } while ((c = in.read()) != -1);
        String token = buffer.toString();
        if (token.isEmpty()) {
            return null;
        }
        return normalizeToken(token);
    }

    private String normalizeToken(String expr) {
        expr = expr.replaceAll("\t", "");
        return expr.trim();
    }

    private boolean isWhitespace(int c) {
        return c == ' ' || c == '\n' || c == '\t' || c == '\r';
    }

    private int nextNonWhitespaceCharacter(PushbackInputStream in) throws IOException {
        int i;
        while((i = in.read()) != -1) {
            char c = (char) i;
            if (!isWhitespace(c)) {
                break;
            }
        }
        return i;
    }

    private void skipToNextLine(PushbackInputStream in) throws IOException {
        int i;
        while((i = in.read()) != -1) {
            char c = (char) i;
            if (c == '\n') {
                break;
            }
        }
    }


    public static void main(String[] args) {
        ConfigurationParser parser = new ConfigurationParser();

        try {
            Configuration conf = parser.parse(new FileInputStream("/Users/golanhall/Projects/xconf/docs/examples/Example.conf"));
//            Configuration conf = parser.parse(new FileInputStream("/Users/golanhall/Projects/xconf/docs/examples/ExampleValidationSchema.conf"));

//            System.out.println(conf.getNodes());
            System.out.println(new ConfigurationWriter().toString(conf));
//            System.out.println(conf.toString().replaceAll(", ", "\n"));

//            PropertyGroup g = conf.getPropertyGroups("VirtualServer").iterator().next();
//            System.out.println(g.getProperties("AllowedMethods").containsValue("POST"));

        } catch (ParseException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    public class PushbackInputStream extends java.io.PushbackInputStream {

        public PushbackInputStream(InputStream in, int size) {
            super(in, size);
        }

        public PushbackInputStream(InputStream in) {
            super(in);
        }

        public int getPosition() {
            return pos;
        }
    }


}
